package com.google.code.sms.controller;

import java.io.OutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.code.sms.domain.MediaElement;
import com.google.code.sms.service.MediaElementService;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import com.google.code.sms.Logger;
import static com.google.code.sms.domain.MediaElement.MediaElementType.AUDIO;
import static com.google.code.sms.domain.MediaElement.MediaElementType.VIDEO;
import com.google.code.sms.domain.Player;
import com.google.code.sms.domain.TransferStatus;
import com.google.code.sms.domain.User;
import com.google.code.sms.io.PlayQueueInputStream;
import com.google.code.sms.service.PlayerService;
import com.google.code.sms.service.SecurityService;
import com.google.code.sms.service.StatusService;
import com.google.code.sms.service.TranscodingService;
import com.google.code.sms.util.StringUtil;

/**
 * A controller which streams the content of a
 * {@link com.google.code.domain.PlayQueue} to a remote {@link Player}.
 *
 * @author Sindre Mehus
 */
public class StreamController implements Controller {

    private static final Logger LOG = Logger.getLogger("StreamController");
    private StatusService statusService;
    private PlayerService playerService;
    private SecurityService securityService;
    private TranscodingService transcodingService;
    private MediaElementService mediaElementService;

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception
    {
        TransferStatus status = null;
        PlayQueueInputStream in = null;
        Player player = playerService.getPlayer(request, response, false, true);
        User user = securityService.getUserByName(player.getUsername());
        TranscodingService.Parameters transcodeParameters;

        try
        {
            // Check user is allowed to stream media.
            if (!user.isStreamRole())
            {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername());
                return null;
            }
            
            // Parameters for transcoding.
            MediaElement mediaElement;
            String format, videoCodec, audioCodec, audioQuality;
            Integer maxAudioChannels, maxSampleRate;
            Integer videoQuality;
            Integer offset;
            
            // Retrieve the Media Element being requested.
            Integer id = ServletRequestUtils.getIntParameter(request, "id");
            if (id == null)
            {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Media Element ID found!");
                return null;
            }
            
            mediaElement = mediaElementService.getMediaElement(id);
            if(mediaElement == null)
            {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Unable to retrieve Media Element.");
                return null;
            }
            
            if(mediaElement.getMediaElementType() == null)
            {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Can't determine media type.");
                return null;
            }
            
            switch(mediaElement.getMediaElementType())
            {
                case VIDEO: case AUDIO:
                    
                    // Transcode Format
                    format = request.getParameter("format");
                    if (format == null)
                    {
                        response.sendError(HttpServletResponse.SC_FORBIDDEN, "No video/audio format specified.");
                        return null;
                    }
                    
                    // Max audio channels.
                    maxAudioChannels = ServletRequestUtils.getIntParameter(request, "maxAudioChannels", 0);
                    if (Integer.valueOf(0).equals(maxAudioChannels))
                    {
                        maxAudioChannels = null;
                    }
                    
                    // Max audio sample rate.
                    maxSampleRate = ServletRequestUtils.getIntParameter(request, "maxSampleRate", 0);
                    if (Integer.valueOf(0).equals(maxSampleRate))
                    {
                        maxSampleRate = null;
                    }
                    
                    break;
                    
                default:
                    response.sendError(HttpServletResponse.SC_FORBIDDEN, "Media Element with ID: " + id + " is not a media file.");
                    return null;
            }
            
            switch(mediaElement.getMediaElementType())
            {
                case VIDEO:
                    
                    // Video codec.
                    videoCodec = request.getParameter("videoCodec");
                    
                    // Retrieve desired audio codec.
                    audioCodec = request.getParameter("audioCodec");
                    
                    // Retrieve video quality.
                    videoQuality = ServletRequestUtils.getIntParameter(request, "videoQuality", 0);
                    if (Integer.valueOf(0).equals(videoQuality))
                    {
                        videoQuality = null;
                    }
                    
                    // Get video offset (seeking).
                    offset = ServletRequestUtils.getIntParameter(request, "offset", 0);
                                        
                    // Compile transcode settings for video.
                    transcodeParameters = new TranscodingService.Parameters(mediaElement, format, videoCodec, audioCodec, maxAudioChannels, maxSampleRate, videoQuality, offset);
                    
                    break;
                    
                case AUDIO:
                    
                    // Retrieve audio quality.
                    audioQuality = request.getParameter("audioQuality");
                    
                    // Compile transcode settings for audio.
                    transcodeParameters = new TranscodingService.Parameters(mediaElement, format, maxAudioChannels, maxSampleRate, audioQuality);
                    
                    break;
                    
                default:
                    response.sendError(HttpServletResponse.SC_FORBIDDEN, "Media Element with ID: " + id + " is not a media file.");
                    return null;
            }

            // Set content type based on requested 'container'.
            response.setContentType(StringUtil.getMimeType(format));
            
            if(mediaElement.isAudio() && getTranscodedSize(transcodeParameters) > 0)
            {
                // Return an estimated transcode size.
                response.setContentLength((int)getTranscodedSize(transcodeParameters));
                
                // Tell client we support byte range requests.
                response.setIntHeader("ETag", mediaElement.getId());
                response.setHeader("Accept-Ranges", "bytes");
            }

            if (request.getMethod().equals("HEAD"))
            {
                return null;
            }

            // Terminate any other streams to this player.
            for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player))
            {
                if (streamStatus.isActive())
                {
                    streamStatus.terminate();
                }
            }

            status = statusService.createStreamStatus(player);

            in = new PlayQueueInputStream(player, status, transcodeParameters, transcodingService, mediaElementService);
            OutputStream out = response.getOutputStream();

            final int BUFFER_SIZE = 2048;
            byte[] buf = new byte[BUFFER_SIZE];

            while (true)
            {
                int n = in.read(buf);

                if (n < 0)
                {
                    break;
                }
                else
                {
                    out.write(buf, 0, n);
                }
            }

        }
        finally
        {            
            if (status != null) 
            {   
                securityService.updateUserByteCounts(user, status.getBytesTransfered(), 0L);
                statusService.removeStreamStatus(status);
            }
            
            IOUtils.closeQuietly(in);
        }
        return null;
    }

    private long getTranscodedSize(TranscodingService.Parameters parameters)
    {
        MediaElement element = parameters.getMediaElement();
        
        Integer duration = element.getMetaData().getDuration();
        Integer channelCount = transcodingService.getTranscodedChannelCount(parameters);

        if (duration == null)
        {
            LOG.warn("Unknown duration for " + element + ". Unable to estimate transcoded size.");
            return 0;
        }
        
        if(channelCount == null)
        {
            LOG.warn("Unknown transcoded channel count for " + element + ". Unable to estimate transcoded size.");
            return 0;
        }
        
        // CBR MP3, AAC and WAV file size can be estimated.
        switch(parameters.getFormat())
        {
            case "mp3": case "mp4":
                
                Integer bitRate = transcodingService.getAudioBitRate(parameters);
                
                // We can only estimate for stereo mp3/aac.
                if(channelCount > 2)
                {
                    return 0;
                }
                
                if (bitRate == null)
                {
                    LOG.error("Unknown bit rate for " + element + ". Unable to estimate transcoded size.");
                    return 0;
                }
                
                return duration * (bitRate * 1000L) / 8L;
             
            // Assumes 16 bits per sample.
            case "wav":
                
                Integer sampleRate = null;
                
                // Determine transcoded sample rate.
                if(parameters.getMaxSampleRate() == null)
                {
                    sampleRate = parameters.getMediaElement().getMetaData().getAudioSampleRate();
                }
                else
                {
                    sampleRate = parameters.getMaxSampleRate();
                    
                    if(sampleRate > parameters.getMediaElement().getMetaData().getAudioSampleRate())
                    {
                        sampleRate = parameters.getMediaElement().getMetaData().getAudioSampleRate();
                    }
                }
                
                if(sampleRate == null)
                {
                    LOG.error("Unknown sample rate for " + element + ". Unable to estimate transcoded size.");
                    return 0;
                }
                
                return sampleRate * channelCount * (16L/8L) * duration;
                
            default:
                return 0;
        }
    }
    
    public void setStatusService(StatusService statusService) {
        this.statusService = statusService;
    }

    public void setPlayerService(PlayerService playerService) {
        this.playerService = playerService;
    }

    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }

    public void setTranscodingService(TranscodingService transcodingService) {
        this.transcodingService = transcodingService;
    }

    public void setMediaElementService(MediaElementService mediaElementService) {
        this.mediaElementService = mediaElementService;
    }
}
